﻿// -----------------------------------------------------------------------
// <copyright file="ArrayGenerator.cs" company="none">
// Copyright (c) 2012 M. Alberti, xanatos(at)live.it
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
// </copyright>
// -----------------------------------------------------------------------

namespace FluentSerializer.Generators
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq.Expressions;
    using System.Reflection;
    using FluentSerializer.CollectionsHelpers;
    using FluentStatement;

    /// <summary>
    /// 
    /// </summary>
    public static class ArrayGenerator
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="serializer"></param>
        /// <param name="parametersType"></param>
        /// <param name="type"></param>
        /// <param name="helper"></param>
        /// <returns></returns>
        [SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:ParameterMustNotSpanMultipleLines", Justification = "More readable this way")]
        public static Descriptor BuildSerializer(Serializer serializer, Type parametersType, Type type, Type helper)
        {
            bool isUniversal = true;

            Type decoratedTypeWithHelper = helper.IsGenericTypeDefinition ? helper.MakeGenericType(type) : helper;
            Type decoratedType = type;
            Type realType = Serializer.UndecorateType(type);

            Type decoratedSubItemType = decoratedType.GetElementType();
            Type realSubItemType = realType.GetElementType();

            ParameterExpression item = Expression.Parameter(realType);
            ParameterExpression parameters = parametersType != null ? Expression.Parameter(parametersType) : null;
            ParameterExpression subItem = Expression.Variable(realSubItemType);

            ConstructorInfo constructor = decoratedTypeWithHelper.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null);
            var helper2 = (ICollectionHelper)constructor.Invoke(null);
            isUniversal &= helper2.IsUniversal;

            Descriptor subItemDescriptor = null;
            Descriptor fixedLengthDescriptor = null;

            if (helper2.IsFixedLength)
            {
                subItemDescriptor = Serializer.GetSerializer(serializer, realSubItemType, decoratedSubItemType, helper);
                isUniversal &= subItemDescriptor.IsUniversal;
            }
            else
            {
                fixedLengthDescriptor = Serializer.GetSerializer(serializer, type, typeof(FixedLength<>).MakeGenericType(type), null);
                isUniversal &= fixedLengthDescriptor.IsUniversal;
            }

            LambdaExpression disposeExpression;
            LambdaExpression deserializeExpression;
            LambdaExpression serializeExpression;
            LambdaExpression sizeExpression;

            /***/
            {
                if (helper2.IsFixedLength && subItemDescriptor.DeserializationHandling.HasFlag(DeserializationHandling.RequireDispose))
                {
                    disposeExpression = Serializer.MakeLambda(ExpressionEx.ForEach(item, subItemDescriptor.DisposeExpression, Expression.Label()), false, item);
                }
                else if (!helper2.IsFixedLength && fixedLengthDescriptor.DeserializationHandling.HasFlag(DeserializationHandling.RequireDispose))
                {
                    disposeExpression = Serializer.MakeLambda(ExpressionEx.ForEach(item, fixedLengthDescriptor.DisposeExpression, Expression.Label()), false, item);
                }
                else
                {
                    disposeExpression = null;
                }
            }

            /***/
            {
                var variables = new List<ParameterExpression>();
                var block = new List<Expression>();

                ParameterExpression size = Expression.Variable(typeof(int));

                variables.Add(size);

                Expression deserializeLength = helper2.DeserializeLength(Serializer.ReaderExpression, Expression.ArrayLength(item));

                if (helper2.IsFixedLength)
                {
                    if (deserializeLength != null)
                    {
                        block.Add(deserializeLength);
                    }

                    Expression subItemDeserialize = Serializer.MakeFromLambda(subItemDescriptor.DeserializeExpression, subItemDescriptor.IsUniversal, Serializer.ReaderExpression, Expression.ArrayAccess(item, size), parameters);

                    if (subItemDescriptor.DeserializationHandling.HasFlag(DeserializationHandling.RequireAssignment))
                    {
                        subItemDeserialize = Expression.Assign(Expression.ArrayAccess(item, size), subItemDeserialize);
                    }

                    ForExpression @for = ExpressionEx.For(Expression.PreIncrementAssign(size), Expression.LessThan(size, Expression.ArrayLength(item)), subItemDeserialize, Expression.Label());

                    block.Add(@for);
                    block.Add(item);
                }
                else
                {
                    block.Add(Expression.Assign(size, deserializeLength ?? Expression.ArrayLength(item)));

                    BlockExpression small, big;

                    {
                        Expression resizeArray = Expression.IfThen(Expression.OrElse(Expression.ReferenceEqual(item, Expression.Constant(null)), Expression.NotEqual(Expression.ArrayLength(item), size)), Expression.Assign(item, Expression.NewArrayBounds(realSubItemType, size)));

                        Expression deserialize = Serializer.MakeFromLambda(fixedLengthDescriptor.DeserializeExpression, fixedLengthDescriptor.IsUniversal, Serializer.ReaderExpression, item, parameters);

                        small = Expression.Block(resizeArray, deserialize);
                    }

                    {
                        Type arrayBufferType = typeof(Serializer.BlockArray<>).MakeGenericType(realSubItemType);

                        ParameterExpression arrayBuffer = Expression.Variable(arrayBufferType);

                        MethodInfo allocateBlock = arrayBufferType.GetMethod("AllocateBlock", BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(int) }, null);
                        PropertyInfo currentBlock = arrayBufferType.GetProperty("CurrentBlock", BindingFlags.Instance | BindingFlags.Public, null, realType, Type.EmptyTypes, null);

                        MethodInfo mathMin = typeof(Math).GetMethod("Min", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(int), typeof(int) }, null);
                        Expression growth = Expression.Constant(Serializer.Growth);

                        UnaryExpression currentBlockLength = Expression.ArrayLength(Expression.Property(arrayBuffer, currentBlock));

                        BinaryExpression assignArrayBuffer = Expression.Assign(arrayBuffer, Expression.New(arrayBufferType.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { realType }, null), item));

                        LabelTarget @break = Expression.Label();

                        WhileExpression @while = ExpressionEx.While(
                            Expression.Constant(true),
                            Expression.Block(new Expression[] 
                            {
                                // Here we play on the fact that the first block (the only one we don't control the size of) is always too much small.
                                Serializer.MakeFromLambda(fixedLengthDescriptor.DeserializeExpression, fixedLengthDescriptor.IsUniversal, Serializer.ReaderExpression, Expression.Property(arrayBuffer, currentBlock), parameters),
                                Expression.SubtractAssign(size, currentBlockLength), 
                                Expression.IfThen(Expression.Equal(size, Serializer.Zero), Expression.Break(@break)),
                                Expression.Call(arrayBuffer, allocateBlock, Expression.Call(mathMin, size, growth)),
                            }),
                            @break);

                        MethodInfo detach = arrayBufferType.GetMethod("Detach");

                        MethodCallExpression arrayBufferDetach = Expression.Call(arrayBuffer, detach);

                        big = Expression.Block(new[] { arrayBuffer }, new Expression[] { assignArrayBuffer, @while, arrayBufferDetach });
                    }

                    if (fixedLengthDescriptor.DeserializationHandling.HasFlag(DeserializationHandling.RequireDispose))
                    {
                        block.Add(Expression.IfThen(Expression.ReferenceNotEqual(item, Expression.Constant(null)), ExpressionEx.ForEach(item, fixedLengthDescriptor.DisposeExpression, Expression.Label())));
                    }

                    block.Add(Expression.Condition(Expression.OrElse(Expression.LessThanOrEqual(size, Expression.Constant(Serializer.Growth)), Expression.AndAlso(Expression.ReferenceNotEqual(item, Expression.Constant(null)), Expression.LessThanOrEqual(size, Expression.ArrayLength(item)))), small, big, realType));
                }

                BlockExpression expression = Expression.Block(variables, block);
                deserializeExpression = Serializer.MakeLambda(expression, isUniversal, Serializer.ReaderExpression, item, parameters);
            }

            /***/
            {
                var block = new List<Expression>();

                Expression sizeSerialize = helper2.SerializeLength(Serializer.WriterExpression, Expression.ArrayLength(item));

                if (sizeSerialize != null)
                {
                    block.Add(sizeSerialize);
                }

                if (helper2.IsFixedLength)
                {
                    Expression subItemSerialize = Serializer.MakeFromLambda(subItemDescriptor.SerializeExpression, subItemDescriptor.IsUniversal, Serializer.WriterExpression, subItem, parameters);
                    Expression forEach = ExpressionEx.ForEach(item, Expression.Lambda(subItemSerialize, subItem), Expression.Label());

                    block.Add(forEach);
                }
                else
                {
                    block.Add(Serializer.MakeFromLambda(fixedLengthDescriptor.SerializeExpression, fixedLengthDescriptor.IsUniversal, Serializer.WriterExpression, item, parameters));
                }

                Expression block2 = block.Count == 1 ? block[0] : Expression.Block(block);
                serializeExpression = Serializer.MakeLambda(block2, isUniversal, Serializer.WriterExpression, item, parameters);
            }

            /***/
            {
                Expression baseSize = helper2.SizeLength(Expression.ArrayLength(item));

                if (helper2.IsFixedLength)
                {
                    if (subItemDescriptor.SizeExpression.Body is ConstantExpression)
                    {
                        Expression size = Expression.Multiply(subItemDescriptor.SizeExpression.Body, Expression.ArrayLength(item));

                        if (baseSize != null)
                        {
                            size = Expression.Add(baseSize, size);
                        }

                        sizeExpression = Serializer.MakeLambda(size, isUniversal, item, parameters);
                    }
                    else
                    {
                        var block = new List<Expression>();

                        ParameterExpression size = Expression.Variable(typeof(int));

                        if (baseSize != null)
                        {
                            block.Add(Expression.Assign(size, baseSize));
                        }
                        else
                        {
                            block.Add(Expression.Assign(size, Serializer.Zero));
                        }

                        Expression subItemSize = Serializer.MakeFromLambda(subItemDescriptor.SizeExpression, subItemDescriptor.IsUniversal, subItem, parameters);
                        ForEachExpression forEach = ExpressionEx.ForEach(item, Expression.Lambda(Expression.AddAssign(size, subItemSize), subItem), Expression.Label());

                        block.Add(forEach);
                        block.Add(size);

                        BlockExpression expression = Expression.Block(new[] { size }, block);
                        sizeExpression = Serializer.MakeLambda(expression, isUniversal, item, parameters);
                    }
                }
                else
                {
                    var block = new List<Expression>();

                    ParameterExpression size = Expression.Variable(typeof(int));

                    if (baseSize != null)
                    {
                        block.Add(Expression.Assign(size, baseSize));
                    }
                    else
                    {
                        block.Add(Expression.Assign(size, Serializer.Zero));
                    }

                    block.Add(Expression.AddAssign(size, Serializer.MakeFromLambda(fixedLengthDescriptor.SizeExpression, fixedLengthDescriptor.IsUniversal, item, parameters)));

                    BlockExpression expression = Expression.Block(new[] { size }, block);
                    sizeExpression = Serializer.MakeLambda(expression, isUniversal, item, parameters);
                }
            }

            var descriptor = new Descriptor(decoratedTypeWithHelper, isUniversal, deserializeExpression, helper2.IsFixedLength ? DeserializationHandling.None : DeserializationHandling.RequireAssignment, serializeExpression, sizeExpression, disposeExpression);
            return descriptor;
        }
    }
}